package abd.pr1.Mappers;


import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;

import com.mysql.jdbc.Statement;


public abstract class AbstractMapper<T,K> {

	protected DataSource ds;

	/**
	 * Devuelve el nombre de la tabla asociada al mapper concreto. Esta
	 * tabla será la utilizada en todas las consultas SQL.
	 * 
	 * @return Cadena con el nombre de la tabla
	 */
	protected abstract String getTableName();

	/**
	 * Devuelve los nombres de las columnas de la tabla asociada al mapper
	 * concreto.
	 * 
	 * @return Array con los nombres de columnas de la tabla.
	 */
	protected abstract String[] getColumnNames();

	/**
	 * Divide un objeto dado en sus componentes. Las componentes del array
	 * devuelto deben estar en el orden correspondiente al dado por las
	 * columnas devueltas por getColumnNames() 
	 * 
	 * @param object Objeto a dividir
	 * @return Componentes del objeto dividido
	 */
	protected abstract Object[] serializeObject(T object);

	/**
	 * Devuelve los nombres de las columnas que forman la clave primaria de
	 * la tabla del mapper concreto.
	 * 
	 * @return Array con nombres de columnas clave
	 */
	protected abstract String[] getKeyColumnNames();
	
	/**
	 * Divide una clave primaria en sus componentes. Las componentes del array
	 * devuelto deben estar en el orden correspondiente al dado por las
	 * columnas devueltas por getKeyColumnNames() 
	 * 
	 * @param key Clave a dividir
	 * @return Componentes de la clave pasada como parámetro
	 */
	protected abstract Object[] serializeKey(K key);
	
	/**
	 * Construye un objeto a partir del resultado de una consulta.
	 * 
	 * @param rs ResultSet con el resultado actual de la consulta.
	 * @return Objeto (de tipo T) representado por la fila contenida en rs
	 * @throws SQLException
	 */
	protected abstract T buildObject(ResultSet rs) throws SQLException;

	/**
	 * Obtiene la clave primaria del objeto pasado como parámetro. 
	 * 
	 * @param object Objeto
	 * @return Clave primera del objeto pasado como parámetro.
	 */
	protected abstract K getKey(T object);

	/**
	 * devuelve true si hay clave autoincrement
	 * @return
	 */
	protected abstract boolean hasAutoincrementKey();
	
	/**
	 * 
	 * @param rs
	 * @param object
	 */
	abstract void updateKeys(ResultSet rs, T object) throws SQLException;
	
	public AbstractMapper(DataSource ds) {
		this.ds = ds;
	}
	
	
	/**
	 * Devuelve la lista de objetos que satisfacen todas las condiciones
	 * del array pasado como parámetro
	 * 
	 * @param conditions Objetos de la clase QueryCondition que especifican las condiciones
	 *                   de los objetos a buscar
	 * @return Lista de objetos de la tabla que cumplen las condiciones dadas. 
	 *         Si ninguno de ellos las cumple, se devuelve una lista vacía.
	 */
	protected List<T> findByConditions(QueryCondition[] conditions) {
		/*
		 * Este método debería hacer uso de los métodos abstractos:
		 * 
		 * getTableName()
		 * getColumnNames()
		 * buildObject()
		 * 
		 * Éste método se declara como protegido, para evitar hacer uso de nombres
		 * de columnas explícitos (contenidos dentro de las QueryCondition) desde
		 * el resto del programa.
		 */
		 
		Connection con        = null;
		PreparedStatement pst = null;
		ResultSet rs          = null;
		List<T> result       = new ArrayList<T>();
		try {
			con = ds.getConnection();
			String[] columnNames = getColumnNames();
			String columnNamesWithCommas = StringUtils.join(columnNames, ", ");
			
			String[] condiciones= new String[conditions.length];
			for (int i = 0; i < conditions.length; i++) {
				condiciones[i]=conditions[i].toString();
			}
			
			//String condstring = StringUtils.join(condiciones,Operator.EQ);
			String sql= "SELECT " + columnNamesWithCommas + " FROM " + getTableName() +  " WHERE " + StringUtils.join(condiciones," AND ");
			
			pst = con.prepareStatement(
					"SELECT " + columnNamesWithCommas + " FROM " + getTableName() +  
					" WHERE " + StringUtils.join(condiciones," AND ")
					);
			
			for (int i = 0; i < condiciones.length; i++) {
				pst.setObject(i+1, conditions[i].getValue());
			}
			
			rs = pst.executeQuery();
			
			while (rs.next()) {
				result.add(buildObject(rs));
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				if (rs != null) rs.close();
				if (pst != null) pst.close();
				if (con != null) con.close();
			} catch (Exception e) {}
		}
		return result;
		  
		 
		 
		//return null;
	}

	public T findById(K key) {
		/*
		 * Este método debería utilizar findByConditions y, además, los siguientes
		 * métodos abstractos:
		 * 
		 * getKeyColumnNames()
		 * serializeKey()
		 * 
		 */
		String[] campos=getKeyColumnNames();
		Object[] valores=serializeKey(key);
		
		if(campos.length>0){
			QueryCondition[] condiciones = new QueryCondition[campos.length];
			
			for (int i = 0; i < valores.length; i++) {
				condiciones[i]= new QueryCondition(campos[i], Operator.EQ, valores[i]);
			}
			
			List<T> resultado=findByConditions(condiciones);
			
			if(resultado.size()>0){
				return resultado.get(0);
			} else {
				return null;
			}
		} else {
			return null;
		}
		
	}

	public void update(T object) {
		/*
		 * Éste método debería utilizar los siguientes métodos abstractos:
		 * 
		 * getTableName()
		 * getColumnNames()
		 * serializeObject()
		 * getKeyColumnNames()
		 * serializeKey()
		 * getKey()
		 *
		 * Este método comparte bastantes cosas en común con findByConditions
		 * y findById (para generar la cláusula WHERE). Deberían extraerse
		 * estas partes comunes en métodos aislados. Si se hace así, las llamadas
		 * a serializeKey y getKeyColumnNames no deberían ser necesarias. 
		 */
		
		Connection con        = null;
		PreparedStatement pst = null;
		ResultSet rs          = null;
		List<T> result       = new ArrayList<T>();
		try {
			con = ds.getConnection();
			String[] columnNames = getColumnNames();
			String[] keyColumnNames = getKeyColumnNames();
						
			
			QueryCondition[] condSet = new QueryCondition[columnNames.length];
			
			Object[] ColumnValues = this.serializeObject(object);
			for (int i = 0; i < columnNames.length; i++) {
				condSet[i]=new QueryCondition(columnNames[i], Operator.EQ, ColumnValues[i]);
			}
			
			String setClause=StringUtils.join(condSet, ", ");
			
			QueryCondition[] condWhere = new QueryCondition[keyColumnNames.length];
						
			Object[] keyValues = this.serializeKey(this.getKey(object));
			for (int i = 0; i < keyColumnNames.length; i++) {
				condWhere[i]=new QueryCondition(keyColumnNames[i], Operator.EQ, keyValues[i]);
			}
			
			String whereClause=StringUtils.join(condWhere, " AND ");
			
			
			pst = con.prepareStatement(
					"UPDATE " + getTableName() + " 	SET " + setClause +  
					" WHERE " + whereClause
					);
			
			String sql= "UPDATE " + getTableName() + " SET " + setClause +  
					" WHERE " + whereClause;
			
			//campos
			for (int i = 0; i < condSet.length; i++) {
				pst.setObject(i+1, condSet[i].getValue());
			}
			
			//claves
			for (int i = condSet.length; i < condWhere.length+condSet.length; i++) {
				pst.setObject(i+1, condWhere[i-condSet.length].getValue());
			}
			
			pst.executeUpdate();			
			
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				if (rs != null) rs.close();
				if (pst != null) pst.close();
				if (con != null) con.close();
			} catch (Exception e) {}
		}
		
		
	}
	
	public boolean delete(T object) {
		
		Connection con        = null;
		PreparedStatement pst = null;
		ResultSet rs          = null;
		//List<T> result       = new ArrayList<T>();
		try {
			con = ds.getConnection();
			
			String[] keyColumnNames = getKeyColumnNames();
						
			QueryCondition[] cond = new QueryCondition[keyColumnNames.length];
						
			Object[] keyValues = this.serializeKey(this.getKey(object));
			for (int i = 0; i < keyColumnNames.length; i++) {
				cond[i]=new QueryCondition(keyColumnNames[i], Operator.EQ, keyValues[i]);
			}
			
			String whereClause=StringUtils.join(cond, " AND ");
			
			pst = con.prepareStatement(
					"DELETE FROM " + getTableName() +  
							" WHERE " + whereClause
					);
			
			String sql= "DELETE FROM " + getTableName() +  
					" WHERE " + whereClause;
			
			for (int i = 0; i < cond.length; i++) {
				pst.setObject(i+1, cond[i].getValue());
			}
			
			pst.executeUpdate();
			return true;				
			
		} catch (SQLException e) {
			e.printStackTrace();
			return false;
		} finally {
			try {
				if (rs != null) rs.close();
				if (pst != null) pst.close();
				if (con != null) con.close();
			} catch (Exception e) {}
		}
		
	}
	
	public boolean insert(T object) {
		
		/*Implementa un método public boolean insert(T objeto) en AbstractMapper que inserte el
		objeto pasado como parámetro en la base de datos. Ten en cuenta que la clave de la tabla pueden
		estar declarada como AUTO_INCREMENT y, por tanto, la sentencia SQL INSERT no debe contener
		valores para dicha clave. Además, en este caso no olvides actualizar el objeto a insertar con la
		clave generada automáticamente por el SGBD.*/
		
		Connection con        = null;
		PreparedStatement pst = null;
		ResultSet rs          = null;
		List<T> result       = new ArrayList<T>();
		try {
			con = ds.getConnection();
			String[] columnNames = getColumnNames();
			String[] keyColumnNames = getKeyColumnNames();
			
			String columnNamesWithCommas;
			String conditionValuesWithCommas;
			
			List<String> validColumnNames = new ArrayList<String>();
			List<Object> validColumnValues = new ArrayList<Object>();
			List<String> valuesCond = new ArrayList<String>();
			
			
			Object[] ColumnValues = this.serializeObject(object);
			
			int nAutoincrementKeyColumns=0;
			
			if(hasAutoincrementKey()){

				nAutoincrementKeyColumns=keyColumnNames.length;
				
			}
			
			for (int i = nAutoincrementKeyColumns; i < columnNames.length; i++) {
				if(ColumnValues[i]!=null){
					validColumnNames.add(columnNames[i]);
					validColumnValues.add(ColumnValues[i]);
					
					valuesCond.add("?");
				}
			}
			
			columnNamesWithCommas = StringUtils.join(validColumnNames, ", ");
			conditionValuesWithCommas = StringUtils.join(valuesCond, ", ");
			
			
			if(hasAutoincrementKey()){

				pst = con.prepareStatement(
						"INSERT INTO " + getTableName() + " ( " + columnNamesWithCommas + " ) " +  
						" VALUES ( " + conditionValuesWithCommas +" ) "
						, Statement.RETURN_GENERATED_KEYS);
				
			} else {
				
				pst = con.prepareStatement(
						"INSERT INTO " + getTableName() + " ( " + columnNamesWithCommas + " ) " +  
						" VALUES ( " + conditionValuesWithCommas +" ) "
						);
			}
			
			String sql= "INSERT INTO " + getTableName() + " ( " + columnNamesWithCommas + " ) " +  
					" VALUES ( " + conditionValuesWithCommas +" ) ";
			
			for (int i = 0; i < valuesCond.size(); i++) {
				pst.setObject(i+1, validColumnValues.get(i));
			}
			
			pst.executeUpdate();
			
			if(hasAutoincrementKey()){
				rs = pst.getGeneratedKeys();				
				if(rs.next()){
					this.updateKeys(rs, object);
				} else return false;
			}
			
			return true;
			
		} catch (SQLException e) {
			e.printStackTrace();
			return false;
		} finally {
			try {
				if (rs != null) rs.close();
				if (pst != null) pst.close();
				if (con != null) con.close();
			} catch (Exception e) {}
		}
	}
	
	public List<T> queryByExample(T prototipo){
		
		String[] campos = this.getColumnNames();
		Object[] valores = this.serializeObject(prototipo);
		
		
		List<QueryCondition> cond = new ArrayList<QueryCondition>();
		
		for (int i = 0; i < campos.length; i++) {
			if(valores[i]!=null){
				cond.add(new QueryCondition(campos[i], Operator.EQ, valores[i]));
			}
		}
		
		QueryCondition[] condiciones = new QueryCondition[cond.size()];
		
		return findByConditions(cond.toArray(condiciones));
		
	}
	
	public List<T> all(){
		
		Connection con        = null;
		PreparedStatement pst = null;
		ResultSet rs          = null;
		List<T> result       = new ArrayList<T>();
		try {
			con = ds.getConnection();
			String[] columnNames = getColumnNames();
			
			String selectClause=StringUtils.join(columnNames, ", ");
			
			pst = con.prepareStatement(
					"SELECT " + selectClause + " 	FROM " + getTableName()
					);
			
			String sql= "SELECT " + selectClause + " 	FROM " + getTableName();
			
		
			rs = pst.executeQuery();			
			
			while (rs.next()) {
				result.add(buildObject(rs));
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				if (rs != null) rs.close();
				if (pst != null) pst.close();
				if (con != null) con.close();
			} catch (Exception e) {}
		}
		return result;
	}
	
}